ADSORPTION

Overview

The ADSORPTION function fits experimental data to established adsorption isotherm models using nonlinear least squares regression. Adsorption isotherms describe how molecules (adsorbates) bind to solid surfaces (adsorbents) at constant temperature, and are fundamental to applications in catalysis, chromatography, water purification, and materials science. For more background on adsorption phenomena, see the Wikipedia article on Adsorption.

This implementation uses scipy.optimize.curve_fit from the SciPy library to perform nonlinear regression via the Levenberg-Marquardt algorithm. The function supports six adsorption models:

  • Brunauer-Emmett-Teller (BET) Multilayer: Extends the Langmuir model to account for multilayer adsorption on surfaces. Developed in 1938 by Brunauer, Emmett, and Teller, this model is widely used for surface area measurements. See BET theory.

  • BET Simplified: A simplified form of the BET equation for specific applications.

  • Freundlich Extended Variable Exponent: An empirical model with a variable exponent that accommodates heterogeneous surfaces and non-ideal adsorption behavior. The Freundlich isotherm is one of the earliest adsorption models (1906).

  • Gunary Phosphate Soil Adsorption: A specialized model for describing phosphate adsorption in soil systems, commonly used in agricultural and environmental science.

  • Langmuir Extended Fractional Power: A modification of the classic Langmuir adsorption model that incorporates a fractional power term for improved flexibility.

  • Langmuir Extended Reciprocal Form: Another Langmuir variant expressed in reciprocal form with an additional exponent parameter.

The function returns optimized parameter values along with their standard errors (derived from the covariance matrix), enabling assessment of fit quality. The classic Langmuir isotherm takes the form:

\theta = \frac{K_{eq} \cdot p}{1 + K_{eq} \cdot p}

where \theta is surface coverage and K_{eq} is the equilibrium constant.

This example function is provided as-is without any representation of accuracy.

Excel Usage

=ADSORPTION(xdata, ydata, adsorption_model)
  • xdata (list[list], required): The xdata value
  • ydata (list[list], required): The ydata value
  • adsorption_model (str, required): The adsorption_model value

Returns (list[list]): 2D list [param_names, fitted_values, std_errors], or error string.

Examples

Example 1: BET multilayer adsorption fitting

Inputs:

adsorption_model xdata ydata
brunauer_emmett_teller_multilayer 0.1 0.411991191769355
1.3250000000000002 -11.066489754739134
2.5500000000000003 -4.0922716541134605
3.7750000000000004 -3.019941985751676
5 -2.93122469020847

Excel formula:

=ADSORPTION("brunauer_emmett_teller_multilayer", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {0.411991191769355;-11.066489754739134;-4.0922716541134605;-3.019941985751676;-2.93122469020847})

Expected output:

a b
2.765 1.068
0.04572 0.0176

Example 2: BET simplified adsorption fitting

Inputs:

adsorption_model xdata ydata
brunauer_emmett_teller_simplified 0.1 0.04031302521196009
1.3250000000000002 -0.5250296303974683
2.5500000000000003 -0.12597014605985662
3.7750000000000004 -0.06481495021686037
5 -0.0597407326617247

Excel formula:

=ADSORPTION("brunauer_emmett_teller_simplified", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {0.04031302521196009;-0.5250296303974683;-0.12597014605985662;-0.06481495021686037;-0.0597407326617247})

Expected output:

a b
2.155 2.092
0.3519 0.6067

Example 3: Freundlich extended exponent fitting

Inputs:

adsorption_model xdata ydata
freundlich_extended_variable_exponent 0.1 0.03760293065948473
1.3250000000000002 3.373393804529696
2.5500000000000003 3.3816488352097918
3.7750000000000004 3.1005998529945566
5 3.068996503762253

Excel formula:

=ADSORPTION("freundlich_extended_variable_exponent", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {0.03760293065948473;3.373393804529696;3.3816488352097918;3.1005998529945566;3.068996503762253})

Expected output:

a b c
2.76 1.218 1.881
0.1373 0.2544 0.1751

Example 4: Gunary phosphate soil adsorption fitting

Inputs:

adsorption_model xdata ydata
gunary_phosphate_soil_adsorption 0.1 0.03338292967433101
1.3250000000000002 0.22376144054819436
2.5500000000000003 0.31582488704502965
3.7750000000000004 0.36772354465575596
5 0.4225541696491852

Excel formula:

=ADSORPTION("gunary_phosphate_soil_adsorption", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {0.03338292967433101;0.22376144054819436;0.31582488704502965;0.36772354465575596;0.4225541696491852})

Expected output:

a b c
2.051 0.9498 2.293
0.6418 0.2719 0.8488

Example 5: Langmuir fractional power fitting

Inputs:

adsorption_model xdata ydata
langmuir_extended_fractional_power 0.1 2.369638270382609
1.3250000000000002 1.3014159334217155
2.5500000000000003 0.9663780982339676
3.7750000000000004 0.7438174832905579
5 0.6698187406430091

Excel formula:

=ADSORPTION("langmuir_extended_fractional_power", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {2.369638270382609;1.3014159334217155;0.9663780982339676;0.7438174832905579;0.6698187406430091})

Expected output:

a b c
2.679 1.186 1.811
0.07742 0.1149 0.05272

Example 6: Langmuir reciprocal form fitting

Inputs:

adsorption_model xdata ydata
langmuir_extended_reciprocal_form 0.1 0.3423934299926415
1.3250000000000002 0.2510648378075721
2.5500000000000003 0.2080267307806113
3.7750000000000004 0.1762085052698202
5 0.16104444468838883

Excel formula:

=ADSORPTION("langmuir_extended_reciprocal_form", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {0.3423934299926415;0.2510648378075721;0.2080267307806113;0.1762085052698202;0.16104444468838883})

Expected output:

a b c
2.763 0.9796 1.795
0.0391 0.06371 0.04309

Python Code

import numpy as np
from scipy.optimize import curve_fit as scipy_curve_fit
import math

def adsorption(xdata, ydata, adsorption_model):
    """
    Fits adsorption models to data using scipy.optimize.curve_fit. See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html for details.

    See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html

    This example function is provided as-is without any representation of accuracy.

    Args:
        xdata (list[list]): The xdata value
        ydata (list[list]): The ydata value
        adsorption_model (str): The adsorption_model value Valid options: Brunauer Emmett Teller Multilayer, Brunauer Emmett Teller Simplified, Freundlich Extended Variable Exponent, Gunary Phosphate Soil Adsorption, Langmuir Extended Fractional Power, Langmuir Extended Reciprocal Form.

    Returns:
        list[list]: 2D list [param_names, fitted_values, std_errors], or error string.
    """
    def _validate_data(xdata, ydata):
        """Validate and convert both xdata and ydata to numpy arrays."""
        for name, arg in [("xdata", xdata), ("ydata", ydata)]:
            if not isinstance(arg, list) or len(arg) < 2:
                raise ValueError(f"{name}: must be a 2D list with at least two rows")
            vals = []
            for i, row in enumerate(arg):
                if not isinstance(row, list) or len(row) == 0:
                    raise ValueError(f"{name} row {i}: must be a non-empty list")
                try:
                    vals.append(float(row[0]))
                except Exception:
                    raise ValueError(f"{name} row {i}: non-numeric value")
            if name == "xdata":
                x_arr = np.asarray(vals, dtype=np.float64)
            else:
                y_arr = np.asarray(vals, dtype=np.float64)

        if x_arr.shape[0] != y_arr.shape[0]:
            raise ValueError("xdata and ydata must have the same number of rows")
        return x_arr, y_arr

    # Model definitions dictionary
    models = {
        'brunauer_emmett_teller_multilayer': {
            'params': ['a', 'b'],
            'model': lambda x, a, b: (a * b * x) / (1.0 + (b - 2.0) * x - (b - 1.0) * np.square(x)),
            'guess': lambda xa, ya: (float(np.mean(ya)), 1.0),
        },
        'brunauer_emmett_teller_simplified': {
            'params': ['a', 'b'],
            'model': lambda x, a, b: x / (a + b * x - (a + b) * np.square(x)),
            'guess': lambda xa, ya: (1.0, 1.0),
        },
        'freundlich_extended_variable_exponent': {
            'params': ['a', 'b', 'c'],
            'model': lambda x, a, b, c: a * np.power(x, b * np.power(x, -c)),
            'guess': lambda xa, ya: (1.0, 1.0, 0.5),
        },
        'gunary_phosphate_soil_adsorption': {
            'params': ['a', 'b', 'c'],
            'model': lambda x, a, b, c: np.divide(x, a + b * x + c * np.sqrt(np.clip(x, 0.0, None)), out=np.zeros_like(x, dtype=float), where=(a + b * x + c * np.sqrt(np.clip(x, 0.0, None))) != 0),
            'guess': lambda xa, ya: (1.0, 1.0, 0.1),
        },
        'langmuir_extended_fractional_power': {
            'params': ['a', 'b', 'c'],
            'model': lambda x, a, b, c: (a * b * np.power(x, 1.0 - c)) / (1.0 + b * np.power(x, 1.0 - c)),
            'guess': lambda xa, ya: (1.0, 1.0, 0.5),
        },
        'langmuir_extended_reciprocal_form': {
            'params': ['a', 'b', 'c'],
            'model': lambda x, a, b, c: 1.0 / (a + b * np.power(x, c - 1.0)),
            'guess': lambda xa, ya: (1.0, 1.0, 0.5),
        }
    }

    # Validate model parameter
    if adsorption_model not in models:
        return f"Invalid model: {str(adsorption_model)}. Valid models are: {', '.join(models.keys())}"

    model_info = models[adsorption_model]

    # Validate and convert input data
    try:
        x_arr, y_arr = _validate_data(xdata, ydata)
    except ValueError as e:
        return f"Invalid input: {e}"

    # Perform curve fitting
    try:
        p0 = model_info['guess'](x_arr, y_arr)
        bounds = model_info.get('bounds', (-np.inf, np.inf))
        if bounds == (-np.inf, np.inf):
            popt, pcov = scipy_curve_fit(model_info['model'], x_arr, y_arr, p0=p0, maxfev=10000)
        else:
            popt, pcov = scipy_curve_fit(model_info['model'], x_arr, y_arr, p0=p0, bounds=bounds, maxfev=10000)

        fitted_vals = [float(v) for v in popt]
        for v in fitted_vals:
            if math.isnan(v) or math.isinf(v):
                return "Fitting produced invalid numeric values (NaN or inf)."
    except ValueError as e:
        return f"Initial guess error: {e}"
    except Exception as e:
        return f"curve_fit error: {e}"

    # Calculate standard errors
    std_errors = None
    try:
        if pcov is not None and np.isfinite(pcov).all():
            std_errors = [float(v) for v in np.sqrt(np.diag(pcov))]
    except Exception:
        pass

    return [model_info['params'], fitted_vals, std_errors] if std_errors else [model_info['params'], fitted_vals]

Online Calculator